.. _глава_56_start:

Object
======

.. index::
    object

Классы, которые явно не наследуют какой-либо класс, автоматически наследуют класс Object
класс. Согласно этому определению, самый верхний класс в любой иерархии классов наследует класс Object

.. code-block:: d

    // ": Object" наследование от Object специально не прописыввается, осуществляется автоматически
    class MusicalInstrument : Object
    {
    // ...
    }
    // Наследование от класса Object - косвенное
    class StringInstrument : MusicalInstrument
    {
    // ...
    }

Поскольку самый верхний класс наследует Object, каждый класс косвенно наследует и Object
также. В этом смысле каждый класс "является" Object.
Каждый класс наследует следующие функции-члены Object:
* toString: Строковое представление объекта.
* opEquals: Сравнение равенства с другим объектом.
* opCmp: Сравнение порядка сортировки с другим объектом.
* toHash: Хеш-значение конкретного объекта данного класса
Последние три функции подчеркивают значения объектов. Они также делают
класс пригодным для использования в качестве ключевого типа ассоциативных массивов.
Поскольку эти функции наследуются, их переопределение для подклассов
требуют ключевого слова override.
*Примечание: Object определяет и другие члены. В этой главе будут рассмотрены только эти
четыре его функции-члена.*

.. index::
    typeid
    TypeInfo

typeid и TypeInfo
-----------------

Object определяется в модуле object (который не является частью пакета std).
В модуле object также определен TypeInfo - класс, предоставляющий информацию о типах. Каждый тип имеет свой объект TypeInfo. Выражение typeid
обеспечивает доступ к объекту TypeInfo, связанному с конкретным типом.
Как мы увидим ниже, класс TypeInfo может быть использован для определения
являются ли два типа одинаковыми, а также для доступа к специальным функциям типа
типа (toHash, postblit и т. д.).
TypeInfo всегда имеет отношение к реальному типу во время выполнения. Например, хотя оба типа
Violin и Guitar ниже наследуют StringInstrument напрямую и
MusicalInstrument косвенно, TypeInfo экземпляров Violin и Guitar
отличаются. Они относятся именно к типам Violin и Guitar соответственно:

.. code-block:: d

    class MusicalInstrument
    {
    }

    class StringInstrument : MusicalInstrument
    {
    }

    class Violin : StringInstrument
    {
    }

    class Guitar : StringInstrument
    {
    }

    void main()
    {
        TypeInfo v = typeid(Violin);
        TypeInfo g = typeid(Guitar);
        assert(v != g); // ← the two types are not the same
    }


Выражения typeid, приведенные выше, используются с такими типами, как сам Violin. typeid
может принимать и выражение, в этом случае он возвращает объект TypeInfo для
типа времени выполнения этого выражения. Например, следующая функция принимает два
параметров разных, но связанных типов:

.. code-block:: d

    import std.stdio;

    // ...
    void foo(MusicalInstrument m, StringInstrument s)
    {
        const isSame = (typeid(m) == typeid(s));
        writefln("The types of the arguments are %s.",
            isSame ? "the same" : "different");
    }
    // ...
    auto a = new Violin();
    auto b = new Violin();
    foo(a, b);

.. index::
    sizeof

Поскольку оба аргумента foo() являются двумя объектами Violin для данного конкретного вызова,
foo() определяет, что их типы одинаковы:

.. code-block:: console

    The types of the arguments are the same.

В отличие от .sizeof и typeof, которые никогда не выполняют свои выражения, typeid
всегда выполняет полученное выражение:

.. code-block:: d

    import std.stdio;

    int foo(string when)
    {
        writefln("Called during '%s'.", when);
        return 0;
    }

    void main()
    {
        const s = foo("sizeof").sizeof; // foo()  не вызывается
        alias T = typeof(foo("typeof")); // foo() не вызывается
        auto ti = typeid(foo("typeid")); // foo() вызывается
    }


Вывод показывает, что выполняется только выражение typeid:

.. code-block:: console

    Called during 'typeid'.

Причина такого различия заключается в том, что реальные типы выражений во время выполнения
могут быть неизвестны до тех пор, пока эти выражения не будут выполнены. Например, точный
точный тип возврата следующей функции будет либо Violin, либо Guitar
в зависимости от значения аргумента функции i:

.. code-block:: d

    MusicalInstrument foo(int i)
    {
        return (i % 2) ? new Violin() : new Guitar();
    }

Существуют различные подклассы TypeInfo для различных типов, таких как массивы,
структуры, классы и т. д. Из них особенно полезным может быть TypeInfo_Class. Например, имя типа времени выполнения объекта можно получить через его свойство
TypeInfo_Class.name в виде строки. Вы можете получить доступ к экземпляру TypeInfo_Class объекта через его свойство .classinfo:

.. code-block:: d

    TypeInfo_Class info = a.classinfo;
    string runtimeTypeName = info.name;



toString
--------

.. index::
    toString

Как и в случае со структурами, toString позволяет использовать объекты класса в строки.

.. code-block:: d

    auto clock = new Clock(20, 30, 0);
    writeln(clock);

Унаследованная функция toString() обычно не полезна; она выдает только имя
типа:

.. code-block:: d

    deneme.Clock

Часть перед именем типа - это имя модуля. Вывод выше
показывает, что Clock определена в модуле deneme.
Как мы видели в предыдущей главе, эта функция почти всегда
переопределяется, чтобы получить более осмысленное строковое представление:

.. code-block:: d

    import std.string;

    class Clock
    {
        override string toString() const
        {
            return format("%02s:%02s:%02s", hour, minute, second);
        }
        // ...
    }

    class AlarmClock : Clock
    {
        override string toString() const
        {
            return format("%s ♫%02s:%02s", super.toString(), alarmHour, alarmMinute);
        }
        // ...
    }

    // ...
    auto bedSideClock = new AlarmClock(20, 30, 0, 7, 0);
    writeln(bedSideClock);

Вывод

.. code-block:: console

    20:30:00 ♫07:00

.. index::
    opEquals

opEquals
--------

Как мы уже видели в главе "Перегрузка операторов" (стр. 298), этот член
функции связан с поведением оператора == (и косвенно оператора !=).
Возвращаемое значение оператора должно быть true, если объекты считаются
равными, и ложным в противном случае.
Предупреждение: Определение этой функции должно быть согласовано с определением функции opCmp(); для
двух объектов, для которых opEquals() возвращает true, opCmp() должна возвращать ноль.
В отличие от структур, компилятор не вызывает a.opEquals(b) сразу же
когда видит выражение a == b. Когда два объекта класса сравниваются с помощью оператора
==, выполняется четырехшаговый алгоритм:

.. code-block:: d

    bool opEquals(Object a, Object b)
    {
        if (a is b)
            return true; // (1)
        if (a is null || b is null)
            return false; // (2)
        if (typeid(a) == typeid(b))
            return a.opEquals(b); // (3)
        return a.opEquals(b) && b.opEquals(a); // (4)
    }


#. Если две переменные предоставляют доступ к одному и тому же объекту (или они обе равны null),
то они равны.
#. Как следует из предыдущей проверки, если только одна из них равна null, то они не равны.
#. Если оба объекта имеют одинаковый тип, то вызывается a.opEquals(b), чтобы
чтобы определить равенство.
#. В противном случае, чтобы два объекта считались равными, opEquals должен быть
определены для обоих типов, а a.opEquals(b) и b.opEquals(a)
должны согласиться с тем, что объекты равны.
Соответственно, если opEquals() не определена для типа класса, то значения объектов
объектов не рассматриваются; скорее, равенство определяется путем проверки того, предоставляют ли
две переменные класса предоставляют доступ к одному и тому же объекту:

.. code-block:: d

    auto variable0 = new Clock(6, 7, 8);
    auto variable1 = new Clock(6, 7, 8);
    assert(variable0 != variable1); // They are not equal
    // because the objects are
    // different

Несмотря на то, что оба объекта построены с помощью одних и тех же аргументов, приведенных выше, их
переменные не равны, потому что они не связаны с одним и тем же объектом.
С другой стороны, поскольку следующие две переменные предоставляют доступ к
одному и тому же объекту, они равны:

.. code-block:: d

    auto partner0 = new Clock(9, 10, 11);
    auto partner1 = partner0;
    assert(partner0 == partner1); // They are equal because
    // the object is the same


Иногда имеет смысл сравнивать объекты по их значениям, а не по
их тождествам. Например, можно предположить, что переменная0 и переменная1
равны, потому что их значения одинаковы.
В отличие от структур, тип параметра opEquals для классов - это Объект

.. code-block:: d

    class Clock
    {
        override bool opEquals(Object o) const
        {
        // ...
        }
    // ...
    }

Как вы увидите ниже, этот параметр почти никогда не используется напрямую. По этой причине
допустимо называть его просто o. Чаще всего первое, что нужно сделать с этим параметром, - это использовать его для преобразования типов.
Параметром opEquals является объект, который появляется в правой части
оператора ==:

.. code-block:: d

    variable0 == variable1; // o represents variable1

Поскольку цель opEquals() - сравнить два объекта типа данного класса, то
первое, что нужно сделать, это преобразовать o в переменную к типу этого класса. Поскольку
не следует изменять объект правой стороны при сравнении на равенствосравнении, то также уместно преобразовать тип в const:

.. code-block:: d

    override bool opEquals(Object o) const
    {
        auto rhs = cast(const Clock)o;
    // ...
    }

Как вы помните, rhs - это общее сокращение для правой части. Кроме того,
std.conv.to также может быть использовано для преобразования:

.. code-block:: d

    import std.conv;
    // ...
    auto rhs = to!(const Clock)(o);

Если исходный объект в правой части может быть преобразован в Clock, то rhs
становится ненулевой переменной класса. В противном случае rhs устанавливается в null, указывая на то, что
что объекты не относятся к одному типу.
В соответствии с дизайном программы может иметь смысл сравнивать объекты
двух несовместимых типов. Здесь я предположу, что для того, чтобы сравнение было корректным,
rhs не должно быть null; поэтому первое логическое выражение в следующем операторе return
проверяет, что оно не равно null. В противном случае будет ошибкой попытка
получить доступ к членам rhs:

.. code-block:: d

    class Clock
    {
        int hour;
        int minute;
        int second;
        override bool opEquals(Object o) const
        {
            auto rhs = cast(const Clock) o;
            return (rhs && (hour == rhs.hour) && (minute == rhs.minute) && (second == rhs.second));
        }
        // ...
    }


С помощью этого определения объекты Clock теперь можно сравнивать по их значениям:

.. code-block:: d

    auto variable0 = new Clock(6, 7, 8);
    auto variable1 = new Clock(6, 7, 8);
    assert(variable0 == variable1); // Теперь они равны
                                    // потому что их значения
                                    // равны

При определении opEquals важно помнить о членах
суперкласса. Например, при сравнении объектов AlarmClock имеет смысл
имеет смысл учитывать и унаследованные члены:

.. code-block:: d

    class AlarmClock : Clock
    {
        int alarmHour;
        int alarmMinute;
        override bool opEquals(Object o) const
        {
            auto rhs = cast(const AlarmClock) o;
            return (rhs && (alarmHour == rhs.alarmHour) && (alarmMinute == rhs.alarmMinute) && super.opEquals(
                    o));
        }
        // ...
    }


Это выражение можно записать и как super == o. Однако это
снова запустит четырехшаговый алгоритм, и в результате код может стать немного
медленнее.

.. index::
    opCmp

opCmp
-----

Этот оператор используется при сортировке объектов класса. opCmp - это функция, которая вызывается
вызывается за кулисами для операторов <, <=, > и >=.
Этот оператор должен возвращать отрицательное значение, если объект слева находится раньше,
положительное значение, если левый объект находится после, и ноль, если оба объекта
имеют одинаковый порядок сортировки.
Предупреждение: Определение этой функции должно быть согласовано с opEquals();
для двух объектов, для которых opEquals() возвращает true, opCmp() должна возвращать ноль.
В отличие от toString и opEquals, в Object нет реализации этой функции по умолчанию
функции в Object. Если реализация недоступна, сравнение объектов для
порядка сортировки приводит к выбросу исключения:


.. code-block:: d

    auto variable0 = new Clock(6, 7, 8);
    auto variable1 = new Clock(6, 7, 8);
    assert(variable0 <= variable1); // ← Causes exception

.. code-block:: console

    object.Exception: need opCmp for class deneme.Clock

От дизайна программы зависит, что произойдет, если левый и правый
объекты разных типов. Один из способов - воспользоваться преимуществами порядка
типов, который поддерживается компилятором автоматически. Это достигается путем вызова
функции opCmp для значений typeid двух типов:

.. code-block:: d

    class Clock
    {
        int hour;
        int minute;
        int second;

        override int opCmp(Object o) const
        {
            /* Taking advantage of the automatically-maintained
            * order of the types. */

            if (typeid(this) != typeid(o))
            {
                return typeid(this).opCmp(typeid(o));
            }

            auto rhs = cast(const Clock) o;
            /* No need to check whether rhs is null, because it is
            * known at this line that it has the same type as o. */

            if (hour != rhs.hour)
            {
                return hour - rhs.hour;
            }
            else if (minute != rhs.minute)
            {
                return minute - rhs.minute;
            }
            else
            {
                return second - rhs.second;
            }
        }
        // ...
    }


В приведенном выше определении сначала проверяется, являются ли типы двух объектов одинаковыми
одинаковыми. Если нет, то используется упорядочивание самих типов. В противном случае сравниваются
объекты по значениям их членов hour, minute и second.
Также может быть использована цепочка тернарных операторов:

.. code-block:: d

    override int opCmp(Object o) const
    {
        if (typeid(this) != typeid(o))
        {
            return typeid(this).opCmp(typeid(o));
        }

        auto rhs = cast(const Clock) o;
        return (hour != rhs.hour
                ? hour - rhs.hour : (minute != rhs.minute
                    ? minute - rhs.minute : second - rhs.second));
    }


Если это важно, сравнение членов суперкласса также должно быть рассмотрено
учитывать. Следующий AlarmClock.opCmp вызывает сначала Clock.opCmp:

.. code-block:: d

    class AlarmClock : Clock
    {
        override int opCmp(Object o) const
        {
            auto rhs = cast(const AlarmClock) o;
            const int superResult = super.opCmp(o);
            if (superResult != 0)
            {
                return superResult;
            }
            else if (alarmHour != rhs.alarmHour)
            {
                return alarmHour - rhs.alarmHour;
            }
            else
            {
                return alarmMinute - rhs.alarmMinute;
            }
        }
        // ...
    }

Выше, если сравнение суперклассов возвращает ненулевое значение, то этот результат
используется, поскольку порядок сортировки объектов уже определен этим значением.
Теперь объекты AlarmClock можно сравнивать по порядку сортировки:

.. code-block:: d

    auto ac0 = new AlarmClock(8, 0, 0, 6, 30);
    auto ac1 = new AlarmClock(8, 0, 0, 6, 31);
    assert(ac0 < ac1);

opCmp используется и другими функциями и библиотеками языка. Например, функция
функция sort() использует преимущества opCmp при сортировке элементов.

**opCmp для строковых членов**

Если некоторые члены являются строками, их можно сравнивать явно, чтобы вернуть
отрицательное, положительное или нулевое значение:

.. code-block:: d

    import std.exception;

    class Student
    {
        string name;
        override int opCmp(Object o) const
        {
            auto rhs = cast(Student) o;
            enforce(rhs);
            if (name < rhs.name)
            {
                return -1;
            }
            else if (name > rhs.name)
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
        // ...
    }


Вместо этого можно использовать существующую функцию std.algorithm.cmp, которая, к тому же, работает быстрее.

.. code-block:: d

    import std.algorithm;

    class Student
    {
        string name
        override int opCmp(Object o) const
        {
            auto rhs = cast(Student) o;
            enforce(rhs);
            return cmp(name, rhs.name);
        }
        // ...
    }


Также обратите внимание, что Student не поддерживает сравнение несовместимых типов, принудительно обеспечивая возможность преобразования из Object в Student.

.. index::
    toHash

toHash
------
Эта функция позволяет использовать объекты типа class в качестве ключей ассоциативного массива. Она
не влияет на случаи, когда тип используется в качестве значений ассоциативного массива. Если эта
функция определена, opEquals также должен быть определен.

**Индексы хэш-таблицы**

Ассоциативные массивы представляют собой реализацию хэш-таблицы. Хеш-таблица - это очень быстрая структура данных
когда дело доходит до поиска элементов в таблице. (Примечание: Как и большинство других
в программном обеспечении, за эту скорость приходится платить: хэш-таблицы должны хранить элементы в неупорядоченном виде.
неупорядоченном виде, и они могут занимать больше места, чем необходимо).
Высокая скорость работы хэш-таблиц обусловлена тем, что сначала они выдают
целочисленные значения для ключей. Эти целые числа называются хэш-значениями. Эти хэш-значения
затем используются для индексации во внутреннем массиве, который хранится в таблице.
Преимущество этого метода заключается в том, что любой тип, который может создавать уникальные целочисленные значения
для своих объектов, может быть использован в качестве ключевого типа ассоциативных массивов. toHash - это функция, которая
функция, возвращающая хэш-значение для объекта.
Даже объекты типа Clock могут использоваться в качестве ключевых значений ассоциативных массивов:

.. code-block:: d

    string[Clock] timeTags;
    timeTags[new Clock(12, 0, 0)] = "Noon";

Определение по умолчанию toHash, унаследованное от Clock, создает различные хэш-значения для разных объектов без учета их значений. Это похоже на
как поведение по умолчанию opEquals рассматривает различные объекты как не равные
Приведенный выше код компилируется и выполняется даже при отсутствии специального определения
toHash для Clock. Однако его поведение по умолчанию почти никогда не является желаемым.
Чтобы увидеть это поведение по умолчанию, давайте попробуем получить доступ к элементу через объект, который
отличным от того, который был использован при вставке элемента. Хотя
новый объект Clock ниже имеет то же значение, что и объект Clock, который был
использовался при вставке в ассоциативный массив выше, это значение не может быть найдено:

.. code-block:: d

    if (new Clock(12, 0, 0) in timeTags)
    {
        writeln("Существует");
    }
    else
    {
        writeln("Отсутствует");
    }

Согласно оператору in, в таблице нет элемента, соответствующего
значению Clock(12, 0, 0):

.. code-block:: console

    Missing

Причина такого удивительного поведения заключается в том, что ключевой объект, который был использован
при вставке элемента, не совпадает с ключевым объектом, который использовался
при доступе к элементу.

**Выбор членов для toHash**

Хотя хэш-значение вычисляется из членов объекта, не каждый
не каждый член подходит для этой задачи.
Кандидатами являются члены, которые отличают объекты друг от друга. Для
например, члены name и lastName класса Student подойдут, еслиэти члены могут быть использованы для идентификации объектов данного типа.
С другой стороны, массив оценок класса Student не подойдет.
как потому, что многие объекты могут иметь такой же массив, так и потому, что массив оценок может меняться со временем.
массив оценок может меняться со временем.

**Вычисление хэш-значений**

Выбор хэш-значений напрямую влияет на производительность ассоциативных
массивов. Более того, вычисление хэша, эффективное для одного типа данных, может
не будет столь же эффективным для другого типа данных.
Поскольку алгоритмы хеширования выходят за рамки этой книги, я приведу лишь одно руководство: В общем случае лучше
создавать разные хэш-значения для объектов, которые, как считается, имеют разные
значения. Однако не является ошибкой, если два объекта с разными значениями выдают
одно и то же значение индекса; это просто нежелательно по соображениям производительности.
Можно предположить, что все члены Clock являются значимыми, чтобы отличать
его объектов друг от друга. По этой причине хэш-значения могут быть вычислены
из значений трех его членов. Количество секунд, прошедших с полуночи, будет
будут эффективными хэш-значениями для объектов, представляющих различные моменты времени:

.. code-block:: d

    class Clock
    {
        int hour;
        int minute;
        int second;
        override size_t toHash() const
        {
            /* Because there are 3600 seconds in an hour and 60
            * seconds in a minute: */
            return (3600 * hour) + (60 * minute) + second;
        }
        // ...
    }

Всякий раз, когда Clock используется в качестве ключевого типа ассоциативных массивов, это специальное определение toHash будет использоваться. В результате, даже если два ключевых объекта
Clock(12, 0, 0), приведенные выше, являются разными, они будут создавать одно и то же хэш-значение
значение.

Новый вывод:

.. code-block:: console

    Exists

Как и в случае с другими функциями-членами, может потребоваться рассмотрение суперкласса также. Например, AlarmClock.toHash может использовать преимущества Clock.toHash
при вычислении индекса:

.. code-block:: d

    class AlarmClock : Clock
    {
        int alarmHour;
        int alarmMinute;
        override size_t toHash() const
        {
            return super.toHash() + alarmHour + alarmMinute;
        }
        // ...
    }


Примечание: Приведенные выше вычисления можно рассматривать только как пример. В общем случае сложение целочисленных значений
не является эффективным способом генерации хэш-значений.
Существуют эффективные алгоритмы вычисления хэш-значений для переменных
с плавающей точкой, массивов и типов struct. Эти алгоритмы доступны и для
программиста.
Все, что нужно сделать, - это вызвать getHash() для typeid каждого члена. Синтаксис
Синтаксис этого метода одинаков для типов с плавающей точкой, массивов и struct.
Например, хэш-значения типа Student можно вычислить из его имени
как показано в следующем коде:

.. code-block:: d

    class Student
    {
        string name;
        override size_t toHash() const
        {
            return typeid(name).getHash(&name);
        }
        // ...
    }


**Хеш-значения для структур**

Поскольку структуры являются типами значений, хэш-значения для их объектов вычисляются
автоматически с помощью эффективного алгоритма. Этот алгоритм учитывает все члены
объекта.
Если есть конкретная причина, например, необходимость исключить определенные члены из
вычисления хэша, toHash() можно переопределить и для структур.

Упражнения
-----------

#. Начните со следующего класса, который представляет собой цветные точки:

.. code-block::

    enum Color
    {
        blue,
        green,
        red
    }

    class Point
    {
        int x;
        int y;
        Color color;
        this(int x, int y, Color color)
        {
            this.x = x;
            this.y = y;
            this.color = color;
        }
    }


Реализуйте opEquals для этого класса таким образом, чтобы игнорировать цвета. Когда
при такой реализации следующая проверка assert должна пройти

.. code-block:: d

    // Different colors
    auto bluePoint = new Point(1, 2, Color.blue);
    auto greenPoint = new Point(1, 2, Color.green);
    // They are still equal
    assert(bluePoint == greenPoint);

#. Реализуйте opCmp, рассматривая сначала x, затем y. Следующие проверки assert
должны пройти:

.. code-block:: d

    auto redPoint1 = new Point(-1, 10, Color.red);
    auto redPoint2 = new Point(-2, 10, Color.red);
    auto redPoint3 = new Point(-2,  7, Color.red);
    assert(redPoint1 < bluePoint);
    assert(redPoint3 < redPoint2);
    /* Even though blue is before green in enum Color,
     * because color is being ignored, bluePoint must not be
     * before greenPoint. */
    assert(!(bluePoint < greenPoint));

Как и в классе Student, описанном выше, вы можете реализовать opCmp, исключив
несовместимые типы с помощью enforce.

#. Рассмотрим следующий класс, объединяющий три объекта Point в массив:

.. code-block:: d

    class TriangularArea
    {
        Point[3] points;
        this(Point one, Point two, Point three)
        {
            points = [one, two, three];
        }
    }


Реализуйте toHash для этого класса. Опять же, следующие проверки assert должны
пройти:

.. code-block:: d

    /* area1 and area2 are constructed by distinct points that
     * happen to have the same values. (Remember that
     * bluePoint and greenPoint should be considered equal.) */
    auto area1 = new TriangularArea(bluePoint, greenPoint, redPoint1);
    auto area2 = new TriangularArea(greenPoint, bluePoint, redPoint1);
    // The areas should be equal
    assert(area1 == area2);
    // An associative array
    double[TriangularArea] areas;
    // A value is being entered by area1
    areas[area1] = 1.25;
    // The value is being accessed by area2
    assert(area2 in areas);
    assert(areas[area2] == 1.25);

Помните, что opEquals также должен быть определен, когда определен toHash.
:ref`решения Объекты<решения Объекты>`:
